package hehe.validation.util;

import java.io.File;
import java.io.FileInputStream;
import java.net.URL;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
import java.util.jar.JarEntry;
import java.util.jar.JarInputStream;

/**
 * 扫描类文件
 *<B>说明：</B>
 *<pre>
 * 略 类详细描述
 *</pre>
 *<B>示例：</B>
 *<pre>
 *  略
 *</pre>
 *<B>日志：</B>
 *<pre>
 *  略
 *</pre>
 *<B>注意事项：</B>
 *<pre>
 *  略
 *</pre>
 */
public class Scan
{
    /**
     * jar 包文件后缀
     *<B>说明：</B>
     *<pre>
     *  略
     *</pre>
     */
    protected static final String JAR_FILE = ".jar";

    /**
     *　class 类文件后缀
     *<B>说明：</B>
     *<pre>
     *  略
     *</pre>
     */
    protected static final String CLASS_FILE = ".class";

    /**
     * 文件名后缀
     *<B>说明：</B>
     *<pre>
     * 不包括. 后文件
     *</pre>
     */
    protected String suffix;

    /**
     * 文件名前缀
     *<B>说明：</B>
     *<pre>
     * 不包括. 后文件
     *</pre>
     */
    protected String prefix;

    /**
     * 扫描路径
     *<B>说明：</B>
     *<pre>
     *  逗号分割支持多个包
     *</pre>
     */
    protected String[] basePackages;

    /**
     * 当前类原始对象
     *<B>说明：</B>
     *<pre>
     *  略
     *</pre>
     */
    protected ClassLoader clazz;

    protected void setSuffix(String suffix)
    {
        this.suffix = suffix;
    }

    protected void setPrefix(String prefix)
    {
        this.prefix = prefix;
    }

    public static Scan create(String... basePackage)
    {
        try {
            return new Scan(basePackage);
        } catch (Exception ex) {
            return null;
        }
    }

    public Scan(String... basePackage)
    {
        this.basePackages = basePackage;
        this.clazz = this.getClass().getClassLoader();
    }

    /**
     * 获取扫描到全部类文件路径
     *<B>说明：</B>
     *<pre>
     *  略
     *</pre>
     * @throws Exception
     */
    public List<Class<?>> start()
    {
        List<Class<?>> nameList = new ArrayList<>();

        for (int i = 0;i < this.basePackages.length;i++) {
            doScan(basePackages[i], nameList);
        }

        return nameList;
    }

    /**
     * 扫描包下面的类
     *<B>说明：</B>
     *<pre>
     *  略
     *</pre>
     * @param basePackage 包名
     * @param nameList 类存储列表
     * @throws Exception
     */
    private List<Class<?>> doScan(String basePackage, List<Class<?>> nameList)
    {
        String splashPath = this.dotToSplash(basePackage);

        // 获取类文件的绝对路径
        URL url = this.clazz.getResource(splashPath);
        String filePath = this.getRootPath(url);

        List<String> names = null;
        if (isJarFile(filePath)) {
            names = readFromJarFile(filePath, splashPath);
        } else {
            names = readFromDirectory(filePath);

        }

        String fullName;

        for (String name : names) {
            if (isClassFile(name)) {
                fullName = toFullyQualifiedName(name, basePackage);
                try {
                    Class<?> className = Class.forName(fullName);
                    // 取消内部类
                    if (className.getName().lastIndexOf("$") == -1 && this.usableClassFile(className)) {
                        nameList.add(className);
                    }
                } catch (Exception ex) {
                    // 类不存在,不可能
                }

            } else {
                String newfilePath= filePath + "/" + name;
                File dirname = new File(newfilePath);
                if (dirname.isDirectory()) {
                    String newBasePackage = basePackage + "." + name;
                    // 遇到目录,继续扫描目录
                    doScan(newBasePackage, nameList);
                }
            }
        }

        return nameList;
    }

    /**
     * Convert short class name to fully qualified name.
     *<B>说明：</B>
     *<pre>
     *  e.g., String -> java.lang.String
     *</pre>
     * @param shortName
     * @param basePackage
     */
    private String toFullyQualifiedName(String shortName, String basePackage)
    {
        StringBuilder sb = new StringBuilder(basePackage);
        sb.append('.');
        sb.append(this.trimExtension(shortName));

        return sb.toString();
    }

    /**
     * 读取jar 目录下的文件及目录
     *<B>说明：</B>
     *<pre>
     *  略
     *</pre>
     * @param jarPath jar 文件路径
     * @param splashedPackageName jar 文件路径
     */
    private List<String> readFromJarFile(String jarPath, String splashedPackageName)
    {
        try {
            JarInputStream jarIn = new JarInputStream(new FileInputStream(jarPath));
            JarEntry entry = jarIn.getNextJarEntry();
            List<String> nameList = new ArrayList<>();
            while (null != entry) {
                String name = entry.getName();
                if (name.startsWith(splashedPackageName) && this.isClassFile(name)) {
                    nameList.add(name);
                }

                entry = jarIn.getNextJarEntry();
            }
            return nameList;
        } catch (Exception ex) {
            return new ArrayList<String>();
        }
    }

    /**
     * 添加class 文件名
     *<B>说明：</B>
     *<pre>
     *  类事件处理
     *</pre>
     */
    private boolean usableClassFile(Class<?> clazz)
    {
        boolean result = true;
        if (this.suffix != null) {
            result = clazz.getSimpleName().endsWith(this.suffix);
        }

        if (this.prefix != null) {
            result = clazz.getSimpleName().startsWith(this.prefix);
        }

        return true;
    }

    /**
     * 读取指定目录下的所有文件包括目录
     *<B>说明：</B>
     *<pre>
     *  略
     *</pre>
     * @param path 文件路径
     * @return 文件名称列表
     */
    private List<String> readFromDirectory(String path)
    {
        File file = new File(path);
        String[] names = file.list();

        if (null == names) {
            return null;
        }

        return Arrays.asList(names);
    }

    /**
     * 判断是否class文件
     *<B>说明：</B>
     *<pre>
     *  略
     *</pre>
     * @param name 类路径
     * @return
     */
    private boolean isClassFile(String name)
    {
        return name.endsWith(CLASS_FILE);
    }

    /**
     * 判断是否jar 包
     *<B>说明：</B>
     *<pre>
     *  略
     *</pre>
     * @param name 类路径
     */
    private boolean isJarFile(String name)
    {
        return name.endsWith(JAR_FILE);
    }

    /**
     * 获取资源的完整路径
     *<B>说明：</B>
     *<pre>
     *  "file:/home/whf/cn/fh" -> "/home/whf/cn/fh"
     *  "jar:file:/home/whf/foo.jar!cn/fh" -> "/home/whf/foo.jar"
     *</pre>
     * @param url 资源路径
     */
    private String getRootPath(URL url)
    {
        String fileUrl = url.getFile();
        int pos = fileUrl.indexOf('!');

        if (-1 == pos) {
            return fileUrl;
        }

        return fileUrl.substring(5, pos);
    }

    /**
     * 类或包命令空间字符分隔符"." 替换成"/"
     *<B>说明：</B>
     *<pre>
     *  "cn.fh.lightning" -> "cn/fh/lightning"
     *</pre>
     * @param name 类路径
     */
    private String dotToSplash(String name)
    {
        return name.replaceAll("\\.", "/");
    }

    /**
     * 去掉后缀
     *<B>说明：</B>
     *<pre>
     *  "Apple.class" -> "Apple"
     *</pre>
     * @param name 类路径
     */
    private String trimExtension(String name)
    {
        int pos = name.indexOf('.');
        if (-1 != pos) {
            return name.substring(0, pos);
        }

        return name;
    }
}
